<template>
  <transition name="expand" appear>
    <div class="aime" @click.stop v-if="value">
      <transition name="fade">
        <Toast v-if="isShowToast" :content="toastContent" />
      </transition>
      <div class="top">
        <img :src="imgSource.closeImg" alt="" v-defaultCover @click.stop="closeAime" />
      </div>

      <div class="landing-error" v-if="isLoading || isError">
        <img class="landing" :src="imgSource.loading" alt="" v-if="isLoading" />
        <template v-if="isError">
          <img class="error" :src="imgSource.error" alt="" />
          <span>数据异常请稍后重试！</span>
        </template>
      </div>
      <template v-else>
        <div class="contents" :style="`height:calc(100% - ${otherHeight}px)`">
          <div class="container" ref="contents" @scroll="scroll">
            <template v-for="(item, index) in contents">
              <AimeDialogBox
                v-if="item.componentName === 'AimeDialogBox'"
                :key="item.msg_id + item.role"
                :content="item.content"
                :needTypeWriter="item.needTypeWriter"
                :msgId="item.msg_id"
                :status="item.status"
                :hasRefresh="index === contents.length - 1"
                :steps="item.items"
                :likeStatus="item.likeStatus"
                :chart="item.chart"
                @refresh="refresh"
                @typeStart="typeStart"
                @typeEnd="typeEnd"
                @scrollToBottom="scrollToBottom"
              />
              <UserDialogBox
                v-else-if="item.componentName === 'UserDialogBox'"
                :key="item.msg_id + item.role + index"
                :content="item.content"
                :primarySelection="item.primarySelection"
              />
              <AimeInitContent
                v-else-if="item.componentName === 'AimeInitContent'"
                :icon="icon"
                :title="title"
                :greeting="greeting"
                :recommendContents="recommendContents"
                @clickRecommendContent="clickRecommendContent"
              />
              <SplitLine v-else-if="item.componentName === 'SplitLine'" />
            </template>
          </div>
        </div>
        <div class="bottom">
          <div class="input-container">
            <div class="primary-selection" v-if="primarySelection">
              <img :src="imgSource.primarySelectionIcon" alt="" class="primary-selection-icon" />
              <div class="selection-content">
                <span>“{{ primarySelection }}”</span>
              </div>
              <img
                :src="imgSource.primarySelectionClose"
                alt=""
                class="primary-selection-close"
                @click.stop="clearPrimarySelection"
              />
            </div>
            <textarea
              class="textarea"
              :placeholder="placeholder"
              v-model.trim="inputText"
              @keydown.enter.stop="sendMessage"
            ></textarea>
            <div class="controls">
              <div
                :class="{ 'new-chat': true, 'can-new-chat': canNewChat }"
                title="新建对话"
                @click.stop="newChat()"
              >
                <img v-if="canNewChat" :src="imgSource.newChatNormalImg" alt="" />
                <img v-else :src="imgSource.newChatForbidden" alt="" />
              </div>
              <div
                :class="{ send: true, 'can-send': canSend }"
                title="发送"
                @click.stop="sendMessage"
              >
                <img :src="imgSource.sendImg" alt="" />
              </div>
            </div>
          </div>
          <div class="tips">
            <span>内容基于AI大模型生成，仅供参考，不代表我们的态度或观点</span>
          </div>
        </div>
      </template>
    </div>
  </transition>
</template>

<script>
import AimeInitContent from './AimeInitContent.vue';
import AimeDialogBox from './AimeDialogBox.vue';
import UserDialogBox from './UserDialogBox.vue';
import SplitLine from './SplitLine.vue';
import Toast from './Toast.vue';
import { reqHistory, createChat, chat } from '../apis/aimeApi';
import closeImg from '../assets/images/close.svg';
import AimeLocalforage from '../utils/localforage';
import { getUserId, arrPick, errorReport, debounce } from '../utils/tools';
import { getHistoryUtils, chatUtils } from '../mixins/index.js';
// 上次滚动位置
let lastScrollTop = 0;
export default {
  name: 'PcAime',
  mixins: [getHistoryUtils, chatUtils],
  components: {
    AimeInitContent,
    AimeDialogBox,
    UserDialogBox,
    SplitLine,
    Toast
  },
  props: {
    // 应用ID
    app_id: {
      type: String,
      default: '',
      required: true
    },
    // 是否显示
    value: {
      type: Boolean,
      default: true,
      required: true
    },
    // 输入框提示语
    placeholder: {
      type: String,
      default: '请输入内容',
      required: false
    },
    // 标题
    title: {
      type: String,
      default: 'AIME助手',
      required: true
    },
    // 问候语
    greeting: {
      type: String,
      default: '你好，我是AIME助手。我可以帮您分析股票相关信息',
      required: true
    },
    // 推荐内容
    recommendContents: {
      type: Array,
      default: () => [],
      required: false
    },
    icon: {
      type: String,
      default:
        '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/m/atom/a4a8117f-6b90-48f2-a2c8-92c654a700be.png',
      required: false
    },
    // 选中内容
    primarySelection: {
      type: String,
      default: '',
      required: false
    },
    primarySelectionData: {
      type: Object,
      default: () => ({}),
      required: false
    }
  },
  data() {
    return {
      imgSource: {
        closeImg: closeImg,
        newChatNormalImg:
          '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/m/atom/67a4a6c2-c50f-4b7e-816b-f877996bd222.png',
        newChatForbidden:
          '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/m/atom/98139f76-cb92-4bb4-98a1-7012e3e38fe6.png',
        sendImg:
          '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/m/atom/73744c47-860b-4d92-a5d5-d70eec135398.png',
        primarySelectionClose:
          '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_2/sunmao/2eef454e-1af3-4910-ac0d-57a3b4f217a7.png',
        primarySelectionIcon:
          '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_2/sunmao/931b81ce-33f0-430e-92d4-46d4f6321a50.png',
        loading:
          '//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/m/atom/1a2acf6d-87bb-4aae-aece-22c13b26d4fb.png',
        error: '//s.thsi.cn/cd/b2cweb-trader-esoterica-front/assets/error.2697ca57.svg'
      },
      isLoading: true,
      isError: false,
      // 输入框内容
      inputText: '',
      // 上下文内容
      /**
       *  - role:'ASSISTANT'|'USER'
       *  - msg_id:string
       *  - items:object
       *    - type:'INTENT'|'NODE'|'EXPLAIN'|'CHART'
       *    - body:object
       *      - status:'START'|'ERROR'|'END'
       *      - node_name:string
       *      - node_id:string
       *      - text:string
       *  - chartData:object
       *  - needTypeWriter:boolean
       *  - status:'pending'|'success'|'error'
       *  - content:string
       *  - chart:object
       *    - config:[]
       *    - chart_data:[]
       *  - primarySelection:string
       *  - componentName:'AimeDialogBox'|'UserDialogBox'|'AimeInitContent'
       */
      contents: [],
      // 当前对话ID
      groupId: '',
      page: 1,
      pageSize: 15,
      lock: false,
      isEnd: false,
      groupIdArr: [],
      historyGroupId: '',
      scrollBottomTimer: null,
      isShowToast: false,
      toastContent: '',
      showToastTimer: null
    };
  },
  computed: {
    // 是否可以发送消息
    canSend() {
      return !this.contents.find(item => item.status === 'pending') && this.inputText;
    },
    // 是否可以开启新的聊天
    canNewChat() {
      return (
        !this.contents.find(item => item.status === 'pending') &&
        this.contents?.[this.contents.length - 1]?.componentName !== 'AimeInitContent'
      );
    },
    // 顶部和底部的高度
    otherHeight() {
      const topHeight = 56;
      const notSelectionHeight = 139;
      const hasSelectionHeight = 183;
      return topHeight + (this.primarySelection ? hasSelectionHeight : notSelectionHeight);
    }
  },
  methods: {
    clearPrimarySelection() {
      this.$emit('clearPrimarySelection');
    },
    closeAime() {
      this.$emit('update', false);
    },
    // init为true时，不检查canNewChat
    newChat(init) {
      if (!this.canNewChat && !init) {
        return;
      }
      createChat({ app_id: this.app_id })
        .then(res => {
          if (res.status_code) {
            throw new Error(`创建聊天失败。响应值为${JSON.stringify(res)}`);
          }
          this.groupId = res.data.group_id;
          if (!this.groupIdArr.includes(this.groupId)) {
            this.aimeLocalforage && this.aimeLocalforage.pushGroupId(res.data.group_id);
            const params = [{ componentName: 'AimeInitContent' }];
            if (!init) {
              params.unshift({ componentName: 'SplitLine' });
            } else {
              this.isEnd = true;
            }
            this.contents.push(...params);
            this.scrollToBottom();
          }
        })
        .catch(error => {
          this.isError = true;
          errorReport({
            name: `创建聊天失败${error.name}`,
            message: `入参的app_id为${this.app_id}。${error.message}`,
            stack: error.stack || ''
          });
        });
    },
    // 获取此条信息的状态和对话内容 - items是来自接口的步骤信息
    getStatusContent(items, role = 'ASSISTANT') {
      // 默认状态为error
      let status = 'error';
      let content = '';
      let primarySelection = '';
      // 是否出现了EXPLAIN为start
      let hasExplain = false;
      let explainSuccess = false;
      // 是否出现了CHART为start
      let hasChart = false;
      let chartSuccess = false;
      const chart = {
        config: [],
        chart_data: []
      };
      items.forEach(i => {
        const roleOptions = {
          // ai发送的信息
          ASSISTANT: () => {
            if (i.type === 'EXPLAIN' && i.body.status === 'START') {
              hasExplain = true;
            }
            if (i.type === 'CHART' && i.body.status === 'START') {
              hasChart = true;
            }
            if (i.type === 'EXPLAIN' && i.body.status === 'END' && i.body.text) {
              explainSuccess = true;
              content = i.body.text;
            }
            // 图标只有有end即可，必须保证文字正常展示
            if (i.type === 'CHART' && i.body.status === 'END') {
              chartSuccess = true;
              chart.config = i.body.config;
              chart.chart_data = i.body.chart_data;
            }
          },
          // 用户发送的信息
          USER: () => {
            if (i.type === 'QUESTION' && i.body.query) {
              status = 'success';
              content = i.body.query;
              primarySelection = i.body.ref?.select_content;
            }
          }
        };
        roleOptions[role] && roleOptions[role]();
      });
      if (
        (!hasExplain && !hasChart) ||
        (hasExplain && !explainSuccess) ||
        (hasChart && !chartSuccess)
      ) {
        status = 'error';
      } else {
        status = 'success';
      }
      return {
        status,
        content,
        primarySelection,
        chart
      };
    },
    async getNewGroupId() {
      const groupId = await this.aimeLocalforage.getLastGroupId();
      if (groupId) {
        this.historyGroupId = groupId;
      }
      this.page = 1;
    },
    // 获取历史数据
    /**
     * 获取历史记录数据。
     * 该方法用于从服务器获取历史记录，并将数据处理后添加到当前页面显示。
     *
     * @param {boolean} isFirst - 是否是第一次加载，默认为false。
     */
    async getHistory(isFirst = false) {
      // 如果当前存在锁或已经是最后一页，则直接返回。
      if (this.lock || this.isEnd) {
        return;
      }
      // 设置锁以防止重复请求。
      this.lock = true;
      let lastScrollHeight = 0;
      // 保存当前滚动条的位置和高度，以便在数据更新后恢复滚动位置。
      if (this.$refs.contents) {
        lastScrollTop = this.$refs.contents.scrollTop;
        lastScrollHeight = this.$refs.contents.scrollHeight;
      }
      try {
        // 构建API请求参数。
        const apiParams = {
          page: this.page,
          page_size: this.pageSize,
          params: { app_id: this.app_id, group_id: this.historyGroupId }
        };
        // 发送异步请求获取历史记录数据。
        const res = await reqHistory(apiParams);
        // 检查响应状态码，如果存在错误则抛出异常。
        if (res.status_code) {
          throw new Error(
            `获取历史数据失败。入参为${JSON.stringify(apiParams)}，响应值为${JSON.stringify(res)}`
          );
        }
        // 处理返回的记录数据，提取所需字段并生成新的对象数组。
        const contents = arrPick(res.data.records, 'role|msg_id|items', item => {
          const { status, content, primarySelection, chart } = this.getStatusContent(
            item.items,
            item.role
          );
          return {
            likeStatus: item.feedback?.feedback || '',
            chartData: '',
            needTypeWriter: false,
            status,
            content,
            primarySelection,
            chart,
            componentName: item.role === 'ASSISTANT' ? 'AimeDialogBox' : 'UserDialogBox'
          };
        });
        // 当最新的一个groupid的第一页没有数据，则需要显示初始化内容
        const result = this.judgeFirstpageHasDate(contents);
        if (!result) {
          // 添加初始化内容
          await this.showInitContent();
          return;
        }
        // 将新的历史记录数据添加到当前显示的内容中。
        this.contents.unshift(...contents);
        // 滚动到页面底部。
        this.pageOneScrollToBottom();
        // 处理内容的状态。
        await this.dealContentsStatus(res);
        // 处理渲染后的操作，包括恢复滚动位置等。
        this.handlePostRender(isFirst, lastScrollTop, lastScrollHeight);
      } catch (error) {
        console.log(error)
        // 记录错误信息并设置错误标志。
        if (isFirst) {
          this.isError = true;
        }
        errorReport({
          name: `获取历史数据错误${error.name}`,
          message: error.message || '',
          stack: error.stack || ''
        });
      } finally {
        // 解除锁。
        this.lock = false;
      }
    },
    // inputText为其他交互方式的问句，有此传参则不清空inputText -msgId为刷新消息的msgId
    sendMessage(event, params = { inputText: '', msgId: '', groupId: '', ref: {} }) {
      const { inputText, msgId, groupId, ref } = params;
      // 如果按下的是回车键且没有按下shift键，则阻止默认行为
      if (event?.key === 'Enter' && !event?.shiftKey) {
        event.preventDefault();
      }
      if (!this.canSend && !inputText) {
        return;
      }
      if (!this.groupId) {
        errorReport({
          name: `sendMessage中groupId不存在`,
          message: '',
          stack: ''
        });
        return;
      }
      const refParams =
        ref && Object.keys(ref).length
          ? ref
          : {
              select_content: this.primarySelection,
              data: this.primarySelectionData
            };
      const apiParams = {
        app_id: this.app_id,
        group_id: groupId || this.groupId,
        action: msgId ? 'REFRESH' : 'CREATE',
        msg_id: msgId,
        question: {
          query: inputText || this.inputText,
          ref: refParams
        }
      };
      const { request } = chat(apiParams, {
        onopen: () => {
          this.onopen({ msgId, inputText, refParams, ref });
        },
        onmessage: param => {
          this.onmessage(param);
        },
        onclose: () => {
          this.onclose();
        },
        onerror: error => {
          this.onerror(apiParams, error);
        }
      });
      request.then(res => {
        if (res) {
          this.scrollToBottom();
        } else {
          errorReport({
            name: `sendMessage中错误${error.name}`,
            message: `请求入参为${apiParams}；${error.message || ''}`,
            stack: error.stack || ''
          });
        }
      });
    },
    clickRecommendContent(recommendContent) {
      this.sendMessage(null, { inputText: recommendContent });
    },
    // 滚动到最底部
    scrollToBottom() {
      // 延迟执行，防止获取不到refs，dom由无到有无法用nextTick
      setTimeout(() => {
        if (this.$refs.contents) {
          this.$refs.contents.scrollTop = this.$refs.contents.scrollHeight;
        }
      });
    },
    // 重新请求
    refresh(msgId) {
      const msgIndex = this.contents.findIndex(
        item => item.msg_id === msgId && item.role === 'ASSISTANT'
      );
      if (msgIndex !== -1) {
        const content = this.contents[msgIndex - 1];
        const question = content.items.find(item => item.type === 'QUESTION');
        if (!question) {
          errorReport({
            name: `refresh中问题不存在${error.name}`,
            message: error.message || '',
            stack: error.stack || ''
          });
          return;
        }
        this.sendMessage(null, {
          msgId,
          inputText: question.body.query,
          groupId: content.group_id,
          ref: {
            select_content: question.body.ref?.select_content || '',
            data: question.body.ref?.data || {}
          }
        });
      } else {
        errorReport({
          name: `refresh中msgId不存在${msgId}`,
          message: '',
          stack: ''
        });
      }
    },
    scroll: debounce(function () {
      const { scrollTop } = this.$refs.contents;
      const placeholderHeight = 50;
      // 向下滚动不加载
      if (lastScrollTop > scrollTop && scrollTop < placeholderHeight) {
        this.getHistory();
      }
      lastScrollTop = scrollTop;
    }, 200),
    // 开始打印
    typeStart() {
      this.scrollToBottom();
      clearInterval(this.scrollBottomTimer);
      const intervalTime = 500;
      this.scrollBottomTimer = setInterval(() => {
        this.scrollToBottom();
      }, intervalTime);
    },
    // 结束打印
    typeEnd() {
      this.scrollToBottom();
      clearInterval(this.scrollBottomTimer);
      this.contents.forEach(item => {
        item.needTypeWriter = false;
      });
    },
    showToast(content, time = 1000) {
      this.isShowToast = true;
      this.toastContent = content;
      clearTimeout(this.showToastTimer);
      this.showToastTimer = setTimeout(() => {
        this.isShowToast = false;
      }, time);
    },
    init() {
      this.isError = false;
      getUserId()
        .then(userId => {
          this.aimeLocalforage = new AimeLocalforage(this.app_id, userId);
          return Promise.all([
            this.aimeLocalforage.getLastGroupId(),
            this.aimeLocalforage.getGroupIds()
          ]);
        })
        .then(([groupId, groupIdArr]) => {
          this.groupIdArr = groupIdArr;
          if (groupId) {
            this.groupId = groupId;
            this.historyGroupId = groupId;
            this.getHistory(true).then(() => {
              this.isLoading = false;
            });
          } else {
            this.isLoading = false;
            this.newChat(true);
          }
        })
        .catch(error => {
          errorReport({
            name: `init中错误${error.name}`,
            message: error.message || '',
            stack: error.stack || ''
          });
        });
    }
  },
  events: [
    {
      name: 'clearPrimarySelection',
      label: '清空选中内容',
      description: '清空选中内容'
    },
    {
      name: 'update',
      label: '改变AIME显示状态',
      description: '改变AIME显示状态',
      data: [
        {
          type: Boolean,
          label: '改变AIME显示状态'
        }
      ]
    }
  ],
  emits: ['clearPrimarySelection', 'update'],
  created() {
    this.init();
  },
  watch: {
    value(newVal) {
      if (!newVal) {
        return;
      }
      // 当发生错误的时候，需要重新初始化
      if (this.isError) {
        this.init();
      } else {
        this.scrollToBottom();
      }
    }
  },
  provide() {
    return {
      showToast: this.showToast
    };
  }
};
</script>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.expand-enter-active,
.expand-leave-active {
  transition: all 0.5s ease;
}

.expand-enter,
.expand-leave-to {
  margin-left: -432px;
  transform: translateX(100%);
  opacity: 0;
}
.expand-enter-to,
.expand-leave {
  margin-left: 0;
  transform: translateX(0);
}

.aime {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  width: 432px;
  height: 100%;
  background: #1b1b24;
  box-shadow: -1px 0px 0px 0px rgba(79, 79, 94, 1);
  background-image: url('//i.thsi.cn/staticS3/mobileweb-upload-static-server.img/m/atom/e9ad9602-ce29-41f4-b1a4-daf351f3e509.png');
  background-size: contain;
  background-repeat: no-repeat;
  background-position: top;
  border-radius: 4px;
  .top {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    width: 433px;
    height: 56px;
    flex-shrink: 0;
    z-index: 2;

    img {
      width: 16px;
      height: 16px;
      margin-right: 20px;
      cursor: pointer;
    }
  }
  @keyframes rotate {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  .landing-error {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    width: 100%;
    height: 100%;
    position: absolute;
    z-index: 1;
    flex-shrink: 1;
    font-family: MicrosoftYaHeiUI;
    font-size: 14px;
    color: #909399;
    line-height: 18px;
    font-weight: 400;
    .landing {
      width: 40px;
      height: 40px;
      animation: rotate 1s linear infinite;
    }
    .error {
      width: 100px;
      height: 100px;
    }
  }
  .contents {
    width: 100%;
    padding: 0 10px 10px 16px;
    flex-shrink: 0;
    flex-grow: 1;

    .container {
      min-height: 100%;
      height: 100%;
      overflow-y: scroll;
      overflow-x: hidden;
    }
  }
  .bottom {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex-shrink: 0;

    .input-container {
      width: 400px;
      min-height: 100px;
      background: #1b1b24;
      border: 1px solid rgba(233, 233, 242, 0.05);
      box-shadow: 0 16px 16px 0 rgba(233, 233, 242, 0.05);
      border-radius: 16px;
      padding: 16px 12px 12px 12px;
      flex-shrink: 0;
      .primary-selection {
        box-sizing: border-box;
        width: 376px;
        height: 32px;
        background: rgba(0, 97, 255, 0.08);
        border-radius: 8px;
        padding: 5px 8px;
        display: flex;
        align-items: center;
        margin-bottom: 12px;
        &:hover {
          .primary-selection-close {
            display: block;
          }
        }
        .primary-selection-icon {
          width: 20px;
          height: 20px;
        }
        .selection-content {
          width: 312px;
          font-family: PingFangSC-Regular;
          font-size: 14px;
          color: #3377e8;
          line-height: 22px;
          font-weight: 400;
          margin-left: 4px;
          margin-right: 4px;
        }
        .primary-selection-close {
          width: 20px;
          height: 20px;
          display: none;
          cursor: pointer;
        }
      }
      .textarea {
        width: 100%;
        height: 40px;
        caret-color: #e9e9f2;
        resize: none;
        font-family: PingFangSC-Regular;
        font-size: 16px;
        color: #e9e9f2;
        line-height: 24px;
        font-weight: 400;
        background: #1b1b24;
        margin: 0;
        padding: 0;
        border: none;
        outline: none;
        &::placeholder {
          font-family: PingFangSC-Regular;
          font-size: 16px;
          color: #4f4f5e;
          line-height: 24px;
          font-weight: 400;
        }
      }
      .controls {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .new-chat {
          display: flex;
          justify-content: center;
          align-items: center;
          width: 32px;
          height: 32px;
          border-radius: 8px;
          &.can-new-chat {
            cursor: pointer;
            &:hover {
              background: #111117;
            }
          }
          img {
            width: 20px;
            height: 20px;
          }
        }
        .send {
          display: flex;
          justify-content: center;
          align-items: center;
          width: 44px;
          height: 32px;
          background: rgba(233, 233, 242, 0.2);
          border-radius: 16px;
          &.can-send {
            cursor: pointer;
            background: #e9e9f2;
          }
          img {
            width: 20px;
            height: 20px;
          }
        }
      }
    }
    .tips {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 324px;
      height: 32px;
      flex-shrink: 0;
      span {
        font-family: PingFangSC-Regular;
        font-size: 12px;
        color: #69697a;
        text-align: center;
        line-height: 20px;
        font-weight: 400;
        transform: scale(0.83);
      }
    }
  }
}
</style>
